#!/usr/bin/env python


import os
import re
import sys
import time
import pickle
import errno 
import shlex
import logging
import argparse
import paramiko
import ConfigParser
from mailer import Mailer
from mailer import Message
from tempfile import mkdtemp
from operator import itemgetter
from subprocess import (call, check_call, check_output, STDOUT,
                        CalledProcessError)


def sync_buffers():
    """Force changed blocks to disk."""
    try:
        check_call(sync)
    except (OSError, CalledProcessError) as e:
        logging.error(e)
        pass


def create_dir(path):
    try:
        os.makedirs(path)
        logging.debug('Created %s', path)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise
        else:
            logging.debug('%s already exists', path)


def relay_email(relayhost, relayauth, subject, body,
                recipient, sender_email, log_file):
    try:
        message = Message(From=sender_email,
                          To=recipient,
                          Subject=subject)
        message.Body = body
        message.attach(log_file)
        sender = Mailer(relayhost)
        sender.send(message)
    except Exception as e:
        try:
            logging.error(e)
        finally:
            pass


def create_report(start_time, end_time, rsyncout):
    s = []
    s.append('------ TIME ------\n\n')
    s.append("Started at: %s\n" % time.strftime('%c %Z', time.localtime(start_time)))
    s.append("Ended at: %s\n" % time.strftime('%c %Z', time.localtime(end_time)))
    s.append("Duration: %s minutes\n\n" % int((end_time - start_time) / 60))
    s.append("------ JOB STATISTICS ------\n")
    s.append(rsyncout)
    summary = ''.join(s)
    return summary


def mount_usb(destination):
    '''Mount backup destination and raise exception on failure.'''
    try:
        check_call([mount, destination])
        logging.debug('Mounted %s successfully.', destination)
    except (OSError, CalledProcessError) as e:
        try:
            raise
        finally:
            pass


def check_usb(destination, rootdir, vmxfilename):
    """
    Test vmxfilename exists and rootdir is rw on backup destination.
    """
    try:
        rwaccess = os.access(
                os.path.join(destination, rootdir), os.W_OK|os.R_OK)
        if not os.path.isdir(os.path.join(destination, rootdir)):
            raise Exception('%s not found in %s.' %(rootdir, destination))
        if not os.path.exists(os.path.join(
                destination, rootdir, vmxfilename)):
            raise Exception('%s not found.' %(vmxfilename))
        if not rwaccess:
            raise Exception('%s is not writeable.' %(
                    os.path.join(destination, rootdir)))
        logging.debug('Backup disk validation passed.')
    except Exception as e:
        try:
            raise
        finally:
            try:
                umount_usb(destination)
            except:
                pass


def umount_usb(destination):
    '''Unmount backup destination and raise exception on failure.'''
    try:
        sync_buffers()
        check_call([umount, destination])
        logging.debug('Unmounted %s successfully.', destination)
    except Exception as e:
        try:
            # Review and make a comment about this if you keep it.
            # Its here so on "squeaky clean" that it logs the fact usb
            # unmount failed
            logging.error(e)
            raise
        finally:
            pass


def return_vmdks(vmxfile, vdptype, destination, rootdir):
    """ 
    Returns path to raw (flat) vmdks to mount from vmx file. Assumes
    *-flat.vmdk has same name as disk descriptor file and resides in
    same folder as vmx file.
    """
    try:
        """Create list of flat vmdks."""
        vmdks = []
        re_pattern = '^scsi[\d\d]:[\d\d]\.fileName'
        for line in open(vmxfile).readlines():
            if re.match(re_pattern, line):
                vmdk = line.split()[2].strip('"')
                vmdkpath = os.path.join(destination, rootdir, vmdk)
                if os.path.isfile(vmdkpath):
                    vmdks.append(vmdkpath[:-5] + '-flat' + vmdkpath[-5:])

        """Validate number of vmdks and VDP type."""
        xvmdks = len(vmdks)
        valid_vdp_types = ((0.5, 4), (1.0, 7), (2.0, 13))
        if float(vdptype) not in [ x[0] for x in valid_vdp_types ]:
            raise Exception('%s is not a valid VDP type.' %(vdptype))
        if xvmdks not in [ x[1] for x in valid_vdp_types ]:
            raise Exception('%s vmdks found, should be 4,7 or 13.' %(xvmdks))

        logging.debug('VDPtype %s, %s vmdks found %s', vdptype, xvmdks, vmdks)

        return tuple(vmdks)
    except Exception as e:
        try:
            raise
        finally:
            try:
                umount_usb(destination)
            except:
                pass


def return_vmdk_loopdevs(vmdks, destination):
    """
    Returns a tuple of loop devices with full path using stdout from
    kpartx "Add partition mappings" command.
    """
    try:
        ldevlist = []
        for vmdk in vmdks:
            k = check_output([kpartx, '-a', '-v', vmdk]).splitlines()
            v = [ os.path.join('/dev/mapper', d.split()[2]) for d in k ]
            ldevlist.append(v)
        loopdevs = [ x for sublist in ldevlist for x in sublist ]
        return tuple(loopdevs)
    except Exception as e:
        try:
            raise
        finally:
            try:
                delete_loopdevs(vmdks, destination)
                umount_usb(destination)
            except:
                pass


def return_fstab(loopdevs, vmdks, destination):
    """
    Mount each loop device and search for fstab. Expects /etc to be
    mouned on /. Subprocess call used instead of check_call as mount
    fails when encounters swap or extended partition, will raise
    exception on missing fstab anyway.
    """
    try:
        tmpdir = mkdtemp()
        fstab = None
        fstabpath = os.path.join(tmpdir, 'etc', 'fstab')
        for partition in loopdevs:
            call([mount, '-o', 'ro', partition, tmpdir])
            if os.path.isfile(fstabpath):
                fstab = open(fstabpath).readlines()
                call([umount, tmpdir])
                logging.debug('found fstab in %s', partition)
                break
            else:
                call([umount, tmpdir])
        os.rmdir(tmpdir)
        if fstab == None:
            raise Exception('Could not find fstab in any vmdk partition')
        return fstab
    except Exception as e:
        try:
            raise
        finally:
            try:
                delete_loopdevs(vmdks, destination)
                umount_usb(destination)
            except:
                pass


def return_blkdevinfo(loopdevs, fstab, vmdks, destination):
    """
    Returns a tuple of lists containing the following strings in order:
        0. Path to loop device
        1. blkid label if exists else None
        2. blkid uuid
        3. blkid file system type
        4. fstab mount point
        5. fstab mount options

        E.G.

         ['/dev/mapper/loop0p2',
          'root',
          '8aadb95c-d177-46ed-b2ca-5e24367c9a4a',
          'ext3',
          '/',
          'acl,user_xattr']

    blkid's -i option is used since it bypasses blkid's cache and does
    not return an non-zero exit code when device cannot be identified
    i.e. extended partitions but will exit with non-zero when it cannot
    read the device etc.
    """
    try:
        blkidout = []
        for ldev in loopdevs:
            label = None
            uuid = None
            fstype = None
            b = check_output([blkid, '-i', '-p', ldev]).splitlines()
            for value in b:
                if value.startswith('LABEL='):
                    label = value.split('=')[1]
                if value.startswith('UUID='):
                    uuid = value.split('=')[1]
                if value.startswith('TYPE='):
                    fstype = value.split('=')[1]
            g = [ldev, label, uuid, fstype]
            blkidout.append(g)
            
        blkdevinfo = [x for x in blkidout if x[3] not in ['swap',None]]

        for line in fstab:
            s = line.split()
            fstabdev, mountpoint, mountopts = s[0], s[1], s[3]

            if fstabdev.startswith('LABEL='):
                fstablabel = fstabdev.split('=')[1]
                for i in blkdevinfo:
                    ldevlabel = i[1]
                    if ldevlabel == fstablabel:
                        i.append(mountpoint)
                        i.append(mountopts)

            if fstabdev.startswith('UUID='):
                fstabuuid = fstabdev.split('=')[1]
                for i in blkdevinfo:
                    ldevlabel = i[1]
                    if ldevlabel == fstabuuid:
                        i.append(mountpoint)
                        i.append(mountopts)

            # Add a /dev version but thats going to be hard since the
            # /dev needs to be associated with the loop device early on
            # not needed for VDP as long as it keeps using "LABEL".

        return tuple(blkdevinfo)
    except Exception as e:
        try:
            raise
        finally:
            try:
                delete_loopdevs(vmdks, destination)
                umount_usb(destination)
            except:
                pass


def mount_loopdevs(blkdevinfo, mount_point, vmdks, destination):
    """
    Mounts the loop devices in order of file system hiearachy using
    UUID. Any error raises an exception that will perform cleanup and
    cause the module to exit, since not having all partitions mounted
    is fatal.
    """
    try:
        for blk in sorted(blkdevinfo, key=itemgetter(4)):
            d = blk[0]
            u = blk[2]
            m = os.path.join(mount_point + blk[4])
            o = blk[5]
            check_call([mount, '-o', o, '-U', u, m])
            logging.debug('Mounted %s with UUID %s on %s', d, u, m)
    except Exception as e:
        try:
            raise
        finally:
            try:
                umount_loopdevs(blkdevinfo, mount_point, vmdks, destination)
                delete_loopdevs(vmdks, destination)
                umount_usb(destination)
            except:
                pass


def umount_loopdevs(blkdevinfo, mount_point, vmdks, destination):
    """
    Unmounts the loop devices in reverse file system hiearchy. Errors
    are logged but will attempt to unmount all loop devices before
    raising the first exception.
    """
    first_error = None
    for blk in sorted(blkdevinfo, key=itemgetter(4), reverse=True):
        try:
            sync_buffers()
            m = os.path.join(mount_point + blk[4])
            check_call([umount, m])
            logging.debug('Unmounted %s successfully', m)
        except Exception as e:
            logging.error(e)
            if not first_error:
                first_error = e

    if first_error:
        try:
            raise first_error
        finally:
            try:
                delete_loopdevs(vmdks, destination)
                umount_usb(destination)
            except:
                pass


def delete_loopdevs(vmdks, destination):
    """
    Deletes the vmdks associated loop devices. Errors are logged but
    will attempt deletion of all loop devices before raising the first
    exception.
    """
    first_error = None
    for vmdk in vmdks:
        try:
            sync_buffers()
            check_call([kpartx, '-d', vmdk])
            logging.debug('Removed loop devices for %s successfully', vmdk)
        except Exception as e:
            logging.error(e)
            if not first_error:
                first_error = e

    if first_error:
        try:
            raise first_error
        finally:
            try:
                umount_usb(destination)
            except:
                pass


def ssh_cmd(vdphost, user, passwd, cmd):
    """
    Connect to vdphost and return exit status, stdout and stderr of
    command.
    """
    try:
        client = paramiko.SSHClient()
        client.load_system_host_keys()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(vdphost, username=user, password=passwd, timeout=120)
        channel = client.get_transport().open_session()
        channel.exec_command(cmd)
        exitstatus = channel.recv_exit_status()
        stdout = channel.recv('1024')
        stderr = channel.recv_stderr('1024')
        channel.close()
        return exitstatus, stdout, stderr
    except Exception as e:
        try:
            raise
        finally:
            pass


def get_job_schedule(vdphost, user, passwd,
                     blkdevinfo, mount_point, vmdks, destination):
    """
    Connect to vdphost, run status.dpn to obtain backup schedule and
    extract backup window start and finish times. Retries 6 times at 5
    minute intervals if first attempt fails to retrieve timewindow
    before raising exception in case vdphost is busy running backups.
    """
    # Unfortunately this fails if the VDP server is under heavy IO.
    # paramiko Transport.banner_timeout = 15 is not long enough for the
    # vdphost to respond.  Will need to perhaps do the same as
    # vdp_jobs_running?
    try:
        cmd = status_dpn
        retries = range(6)
        first_error = None
        for attempt in retries:
            try:
                exitstatus, stdout, stderr = ssh_cmd(vdphost, user, passwd, cmd)
                if exitstatus != 0:
                    raise Exception(stderr.decode('utf-8'))
                break
            except Exception as e:
                logging.error(e)
                attempt + 1
                if not first_error:
                    first_error = e
                time.sleep(300)

        if first_error:
            try:
                raise first_error
            finally:
                pass

        logging.debug('status.dpn ran successfully')
        start = int(stdout.splitlines()[-3].split()[-3].split(':')[0])
        finish = int(stdout.splitlines()[-2].split()[-3].split(':')[0])
        timewindow = range(start,24) + range(0,finish)
        logging.debug('Time window is %s', timewindow)
        return timewindow
    except Exception as e:
        try:
            raise
        finally:
            try:
                umount_loopdevs(blkdevinfo, mount_point, vmdks, destination)
                delete_loopdevs(vmdks, destination)
                umount_usb(destination)
            except:
                pass


def vdp_jobs_running(vdphost, user, passwd):
    """
    # Issue with using force and receiving no router to host (ie
    # connection cannot be established to vdphost - it will run
    # forever and user wont be notified.

    Connect to vdphost and check for running backups. If backups are
    running returns True else returns False. Exceptions are logged and
    return True to ensure subsequent processes do not run if temporary
    connection issue (such as heavy IO during the vdphost's backup) or
    error condition exists on vdphost.
    """
    try:
        cmd = mccli + ' activity show --active=true'
        exitstatus, stdout, stderr = ssh_cmd(vdphost, user, passwd, cmd)
        if exitstatus != 0:
            raise Exception(stderr.decode('utf-8'))
        logging.debug('Obtain vdp job status succeeded')
        if stdout.splitlines()[3]:
            return True
        else:
            return False
    except Exception as e:
        try:
            logging.warning(e)
        finally:
            return True


def create_checkpoint(vdphost, user, passwd):
    """
    Connect to vdphost and create a checkpoint. Exceptions are logged
    as warnings but processing continues as checkpoint creation failure
    is not fatal.
    """
    try:
        cmd = mccli + ' checkpoint create --override_maintenance_scheduler'
        exitstatus, stdout, stderr = ssh_cmd(vdphost, user, passwd, cmd)
        if exitstatus != 0:
            raise Exception(stderr.decode('utf-8'))
        logging.debug('Checkpoint created successfully')
    except Exception as e:
        try:
            logging.warning(e)
        finally:
            pass


def dpnctl_stop(vdphost, user, passwd):
    try:
        cmd = dpnctl + ' --ems_shutdown stop all'
        exitstatus, stdout, stderr = ssh_cmd(vdphost, user, passwd, cmd)
        if exitstatus != 0:
            raise Exception(stderr.decode('utf-8'))
        logging.debug('dpnctl subsystems stopped successfully')
    except Exception as e:
        try:
            raise
        finally:
            try:
                time.sleep(60)
                dpnctl_start(vdphost, user, passwd)
            except:
                pass


def dpnctl_start(vdphost, user, passwd):
    try:
        cmd = dpnctl + ' start all'
        exitstatus, stdout, stderr = ssh_cmd(vdphost, user, passwd, cmd)
        if exitstatus != 0:
            raise Exception(stderr.decode('utf-8'))
        logging.debug('dpnctl subsystems started successfully')
    except Exception as e:
        try:
            raise
        finally:
            try:
                logging.error('dpnctl_start error, cleaning up...')
            except:
                pass


def sync_vdp(vdphost, options, exclude, user, mount_point):
    output = None
    try:
        o = shlex.split(options)
        e = ['--exclude=%s' % x.strip() for x in exclude.split(',')]
        r = ([rsync] + o + e + [user + '@' + vdphost + ':/', mount_point])
        logging.debug('Rsync command: %s', r)
        #r = ([rsync] + o + e + [user + '@' + vdphost + ':/etc', '/tmp/etc'])
        output = check_output(r, stderr=STDOUT)
        logging.info('Rsync of VDP completed successfully')
        logging.info(output.decode('utf-8'))
        return output.decode('utf-8')
    except Exception as e:
        try:
            raise
        finally:
            try:
                logging.error('Rsync of VDP encountered and error')
                logging.error(output.decode('utf-8'))
                # i think this is wrong and needs testing.
                return output.decode('utf-8')
            except:
                pass


def squeaky(vmdks, blkdevinfo, mount_point, destination):
    try:
        umount_loopdevs(blkdevinfo, mount_point, vmdks, destination)
        delete_loopdevs(vmdks, destination)
        umount_usb(destination)
        return True
    except Exception as e:
        return False
        

def main():

    log_file = '/var/log/syncvdp.log'
    var_tmp = '/var/tmp'
    job_success = None
    # look at better way to set this.  Using None causes TypeError:
    # sequence item 5: expected string, NoneType found in creat_report
    # simplest option may be to use if None then "no rsync output" in
    # email report.
    rsyncout = 'No Output logged'

    """Setup argparse."""
    parser = argparse.ArgumentParser()

    parser.add_argument('-c', '--config-file',
            help='path to config file, default = /etc/syncvdp/syncvdp.ini',
            default='/etc/syncvdp/syncvdp.ini')

    parser.add_argument('-l', '--log-level',
            help='set logging level',
            default='INFO',
            choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])    

    parser.add_argument('-d', '--dry-run',
            help='Perform dry run',
            action='store_true')

    parser.add_argument('-x', '--cleanup',
            help='Clean up loop devices and mounts.',
            action='store_true')

    parser.add_argument('-f', '--force',
            help='Run backup outside of time window',
            action='store_true')

    parser.add_argument('-n', '--no-email',
            help='Disable email reports',
            action='store_true')

    """Create variables from argparse."""
    args = parser.parse_args()
    log_level = args.log_level
    config_file = args.config_file
    dry_run = args.dry_run
    cleanup = args.cleanup
    force = args.force
    no_email = args.no_email

    logging.basicConfig(
            filename=log_file, format='%(asctime)s %(levelname)s: %(message)s',
            datefmt='%F %H:%M:%S', filemode='w',
            level=log_level)

    config = ConfigParser.ConfigParser()
    try:
        if not os.path.isfile(config_file):
            raise Exception('Cannot find config file: %s' %(config_file))
        config.read(config_file)
    except Exception as e:
        logging.error(e)
        return 1

    
    try:
        general = dict(config.items('GENERAL'))
        if no_email:
            relayhost = False
        else:
            relayhost = general.setdefault('smtp_relay', False)
        sender_email = general.setdefault('smtp_sender', None)
        recipient = general.setdefault('smtp_recipients', None).split()
        if dry_run:
            # this will fail if subject not set
            subject = general.setdefault('smtp_subject', None) + ' !!DRY RUN!!'
        else:
            subject = general.setdefault('smtp_subject', None)
        relayauth = general.setdefault('smtp_auth', None)

        global rsync, blkid, kpartx, sync, mount, umount, dpnctl, mccli, status_dpn
        rsync = general.setdefault('rsync_path', '/usr/bin/rsync')
        blkid = general.setdefault('blkid_path', '/sbin/blkid')
        kpartx = general.setdefault('kpartx_path', '/sbin/kpartx')
        sync = general.setdefault('sync_path', '/bin/sync')
        mount = general.setdefault('mount_path', '/bin/mount')
        umount = general.setdefault('umount_path', '/bin/umount')
        dpnctl = general.setdefault('dpnctl_path', '/usr/local/avamar/bin/dpnctl')
        mccli = general.setdefault('mccli_path', '/usr/local/avamar/bin/mccli')
        status_dpn = general.setdefault('statusdpn_path', '/usr/local/avamar/bin/status.dpn')

        vdp_sections = [x for x in config.sections() if x not in ['GENERAL']]
        vdp_jobs = {}
        for section in vdp_sections:
            vdp_jobs[section] = dict(config.items(section))
        xvdp_jobs = len(vdp_jobs)
        logging.debug('Found %s backup jobs: %s', xvdp_jobs, vdp_sections)
    except Exception as e:
        logging.error(e)
        return 1


    start_time = time.time()


    # SHOULD I PICKLE EVERYTHING OF EACH JOB INTO A FILE FOR UNPICKLING LATER
    # THIS COULD BE USED FOR THREADS AND FOR CLEANUP?
    for key, value in vdp_jobs.iteritems():
        try:
            destination = value.get('usb_mount')
            rootdir = value.get('name')
            vmxfilename = value.get('vmx_file')
            vdptype = value.get('type')
            vdphost = value.get('host')
            user = value.setdefault('user', 'root')
            passwd = value.setdefault('passwd', None)
            if dry_run:
                options = value.setdefault('rsync_options', '-ahHAX -e ssh --stats --delete') + ' --dry-run --exclude=/mnt/axion'
            else:
                options = value.setdefault('rsync_options', '-ahHAX -e ssh --stats --delete')
            exclude = value.setdefault('exclude', '/proc,/dev,/sys,/mnt/auto/cdrom')

            mount_point = os.path.join('/mnt', rootdir)
            vmxfile = os.path.join(destination, rootdir, vmxfilename)

            """Create var_tmp and mount_point if non existent."""
            create_dir(mount_point)

            if cleanup:
                # for multiple hosts this is broken as are the pickles
                # could use and ismount for cleanup to only clean mounted loopdevs?
                vmdks = pickle.load(open(os.path.join(var_tmp, 'vmdks.pkl'), 'rb'))
                blkdevinfo = pickle.load(open(os.path.join(var_tmp, 'blkdevinfo.pkl'), 'rb'))
                cleanup_result = squeaky(vmdks, blkdevinfo, mount_point, destination)
                if cleanup_result:
                    return 0
                else:
                    return 1
    
    
            """Mount backup disk if not already mounted."""
            dest_mounted = os.path.ismount(destination)
            if not dest_mounted:
                logging.debug('Attempting to mount %s', destination)
                mount_usb(destination)
            else:
                logging.debug('%s already mounted', destination)
                
            """Check vmxfile exists."""
            check_usb(destination, rootdir, vmxfilename)
    
            """Save vmdks object to disk after creation."""
            vmdks = return_vmdks(vmxfile, vdptype, destination, rootdir)
            pickle.dump(vmdks, open(os.path.join(var_tmp, 'vmdks.pkl'), 'wb'))
    
            loopdevs = return_vmdk_loopdevs(vmdks, destination)
            fstab = return_fstab(loopdevs, vmdks, destination)
    
            """Save blkdevinfo object to disk after creation."""
            blkdevinfo = return_blkdevinfo(loopdevs, fstab, vmdks, destination)
            pickle.dump(
            blkdevinfo, open(os.path.join(var_tmp, 'blkdevinfo.pkl'), 'wb'))
    
            """Mount loop devices"""
            mount_loopdevs(blkdevinfo, mount_point, vmdks, destination)
    
            if force:
                timewindow = range(0,24)
            else:
                timewindow = get_job_schedule(vdphost, user, passwd,
                                              blkdevinfo, mount_point, vmdks,
                                              destination)

    
            #This code should allow me to sleep/sched until timewindow opens
            #But not cause the program to keep running endlessly.
            #if now in timewindow:
            #    print 'the time is now'
            #else:
            #    if now < timewindow[0]:
            #        print timewindow[0] - now
            #    else:
            #        print timewindow[0] - now + 24
            # convert hours to seconds 'hour' * 60 * 60
    
            while True:
                if time.localtime()[3] in timewindow:
                    if vdp_jobs_running(vdphost, user, passwd):
                        time.sleep(900)
                    else:
                        # sleep to allow and processes to complete in
                        # case last loop completed close to end of 
                        # backup jobs.
                        if not dry_run:
                            time.sleep(60)
                            create_checkpoint(vdphost, user, passwd)
                            dpnctl_stop(vdphost, user, passwd)
                        else:
                            logging.info('create_checkpoint and dpnctl_stop skipped during dry run.')
                        rsyncout = sync_vdp(vdphost, options, exclude, user, mount_point)
                        if not dry_run:
                            dpnctl_start(vdphost, user, passwd)
                        else:
                            logging.info('dpnctl_start skipped during dry run.')
                        umount_loopdevs(blkdevinfo, mount_point, vmdks, destination)
                        delete_loopdevs(vmdks, destination)
                        if not dest_mounted:
                            logging.debug('Attempting to unmount %s', destination)
                            umount_usb(destination)
                        else:
                            logging.debug('%s left mounted', destination)
                        job_success = True
                        break
                else:
                    logging.info('Outside of time window')
                    umount_loopdevs(blkdevinfo, mount_point, vmdks, destination)
                    delete_loopdevs(vmdks, destination)
                    if not dest_mounted:
                        logging.debug('Attempting to unmount %s', destination)
                        umount_usb(destination)
                    else:
                        logging.debug('%s left mounted', destination)
                    job_success = False
                    break
            return 0
    
        except Exception as ex:
            logging.exception(ex)
            job_success = False
            # change this to a switch and place return in finally?
            # e.g. global_result = False
            # if global result false:
            #    return 1
            # this would also let me set subject line i think
            return 1
    
        finally:
            # need to re-work cleanup processes and dest_mounted
            end_time = time.time()
            body = create_report(start_time, end_time, rsyncout)
    
            if relayhost:
                relay_email(relayhost, relayauth, subject, body,
                        recipient, sender_email, log_file)


if __name__ == '__main__':
    sys.exit(main())

